// Requires date.js be loaded for time zone calculations

var WeekView  = Class.create({

    /**
     * Internal version number
     * @type    decimal
     */
    version: 0.2, 
    
    /**
     * Object hash of open time slots
     *
     * Example:
     * openSlots = Object { '11/10/2009'
     *                          => '08:00:00 -05:00'
     *                                  => 'slot_count' = 1,
     *                                      'slots' => Array(0 => 1) }
     *
     * JSON Example: {"openTimeSlots":{"11\/10\/2009":{"08:00:00 -05:00":{"slot_count":1,"slots":["1"]}                                     
     * Default: null
     * @type    Object
     */
    openSlots : null,
    
    /**
     * Object hash of close time slots
     *
     * Same format as openSlots
     * Default: null
     * @see     openSlots
     * @type    Object
     */
    closedSlots : null,
    
    /**
     * Names for the days of the week
     * @type    array
     **/
    dayNames : ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
    
    /**
     * Names for the months of the year
     * @type    array
     */
    monthNames : ['January','February','March','April','May','June','July','August','September','October','November','December'],
    
    /**
     * Number of milliseconds in a day
     * @type    long
     */
    MILLIS_IN_DAY : 86400000,
    
    /**
     * Number of milliseconds in a week
     * @type    long
     */
    MILLIS_IN_WEEK : (this.MILLIS_IN_DAY * 7),
    
    /**
     * Indicator for daylight savings time
     * @type    boolean
     */
    isDaylightSavingsTime : false,

    options : {
        
        /**
         * Date the week view starts at
         * Default: Current date and time
         * @type    Date
         */
        date: new Date(),
        
        /**
         * Time slots per that are displayed on the main week view calendar
         * Default: 1
         * TODO: fix this feature
         * @type    integer
         */
        timeslotsPerHour: 1,
        
        /**
         * Time slot height in pixels
         * Default: 20
         * @type    integer
         */
        timeslotHeight: 20,
        
        /**
         * Number of "slots" when drilling down on a time slot
         * When you click on an hour this is how many sub items apear in the drop down
         * Default: 4
         * @type    integer
         * @deprecated see scheduleInterval
         */
        drillDownSlots: 4,
        
        /**
         * The scheduling interval must be specified as either
         * 15 - drillDownSlots = 4
         * 30 - drillDownSlots = 2
         * 60 - drillDownSlots = 1
         * TODO: Replace drill down slots
         * Default: 15
         * @type    integer
         */
        scheduleInterval : 15,
        
        /**
         * Hour to start at on the left column. 24 Hour format
         * Default: 0
         * @type    integer
         */
        startHour: 0,
        
        /**
         * Hour to stop at on the left column. 24 Hour format
         * Default: 24
         * @type    integer
         */
        stopHour: 24,
        
        /**
         * HTML element of the container to hold the navigation, header, and calendar
         * Default: null
         * @type    Element
         */
        container : null,
        
        /**
         * HTML element of the container to hold the calendar
         * Default: null
         * @type    Element
         */
        calendarContainer : null,
        
        /**
         *  Time each spot will be reserved for.
         *  For example when choosing a time slot for a survey start time this
         *  value would indicate the survey length
         *  Default: 60
         *  @type   integer
         */
        reserveTime : 60,
        
        /**
         * Break time in between reservations
         * For example when doing surveys this would be the moderators break
         * time
         * Default: 15
         * @type    integer
         */
        breakTime : 15,
        
        /**
         * To impose a hard limit on the maximum number of scheduled interviews
         * per day set this can be set
         * Default: 0
         * @type    integer
         */
        maxScheduledPerDay : 0,

        /**
         * This is the time zone the week view is the rendered in specified as
         * GMT offset
         * Default: 0
         */
        GMTOffset : 0,
        
        /**
         * Events are required to use the functionality of the week view calendar
         * They allow you to act on the actions
         */
        
        /**
         * Called when the week view is rendered
         * @returns void
         */
        onRender : function(calendar){} ,
        
        /**
         * Called after the navigate previous is triggered and a new week is
         * going to be rendered
         * @param   weekView Object Weekview object
         * @param   event   Event   Event object that triggered this event
         * @param   d       Date    Date the new week starts at
         * @param   ed      Date    Date the new week ends at
         * @param   t       Date    Time the new week starts at
         * @param   et      Date    Time the new week ends at
         * @returns void
         */         
        onNavigatePrevious : function(weekView,event,d,ed,t,et){},
        
        /**
         * Called after the navigate next is triggered and a new week is
         * going to be rendered
         * @param   weekView Object Weekview object
         * @param   event   Event   Event object that triggered this event
         * @param   d       Date    Date the new week starts at
         * @param   ed      Date    Date the new week ends at
         * @param   t       Date    Time the new week starts at
         * @param   et      Date    Time the new week ends at
         * @returns void
         */  
        onNavigateNext :  function(weekView,event,d,ed,t,et){},
        
        /**
         * Called when the time zone is changed
         * @param   weekView Object Weekview object
         * @param   event   Event   Event object that triggered this event
         * @param   d       Date    Date the new week starts at
         * @param   ed      Date    Date the new week ends at
         * @param   t       Date    Time the new week starts at
         * @param   et      Date    Time the new week ends at
         * @returns void
         */  
        onTimeZoneChange :  function(weekView,event,d,ed,t,et){},
        
        /**
         * Called when a slot is selected
         * @param   event   Event   Event object that triggered this event
         * @param   d       Date    Date and time that were selected
         */
        onSlotSelect :  function(event,d){}
        
    },

    /**
     * Called when the class is initalized
     *
     * @param   object  options     Options to override
     * @returns void
     */
    initialize: function(options) {
        Object.extend(this.options, options);
        this.options.scheduleInterval = parseInt(this.options.scheduleInterval, 10);
        this.clearTime(this.options.date);
    },

    /**
     * Render the calendar
     *
     * @param   object  el      HTML element to hold the calendar
     * @returns void
     */
    render : function(el) {
        var cHtml = ''; // Calendar HTML
        this.options.container = el;
        var options = this.options;
        // Clear our containers current contents
        this.options.container.update('');
        this.calendarContainer = $(this.options.container).insert("<div class=\"week-calendar\">", {position:'top'}).down();
        var timeZoneName = this.options.date.getTimezoneName();
        this.GMTOffset = this.options.date.getGmtOffset();
        this.isDaylightSavingsTime = false;
        if (timeZoneName.match(/daylight|summer/i)
            || timeZoneName.match(/DT$/)) {
            this.isDaylightSavingsTime = true;
        }

        // render calendar controlls
        var timeZoneOptions;
        // Generate time zone names based on daylight savings time condition
        // Only US zones are supported right now
        this.hTimeZones = new Hash();
        if (this.isDaylightSavingsTime) {
            this.hTimeZones.set('-04:00', 'Eastern Daylight Time (GMT -04:00)');
            this.hTimeZones.set('-05:00', 'Central Daylight Time (GMT -05:00)');
            this.hTimeZones.set('-06:00', 'Mountain Daylight Time (GMT -06:00)');
            this.hTimeZones.set('-07:00', 'Pacific Daylight Time (GMT -07:00)');
            this.hTimeZones.set('-08:00', 'Alaska Daylight Time (GMT -08:00)');
            this.hTimeZones.set('-09:00', 'Hawaii-Aleutian Daylight Time (GMT -09:00)');
        } else {
            this.hTimeZones.set('-05:00', 'Eastern Standard Time (GMT -05:00)');
            this.hTimeZones.set('-06:00', 'Central Standard Time (GMT -06:00)');
            this.hTimeZones.set('-07:00', 'Mountain Standard Time (GMT -07:00)');
            this.hTimeZones.set('-08:00', 'Pacific Standard Time (GMT -08:00)');
            this.hTimeZones.set('-09:00', 'Alaska Standard Time (GMT -09:00)');
            this.hTimeZones.set('-10:00', 'Hawaii-Aleutian Standard Time (GMT -10:00)');
        }

        this.hTimeZones.each(function(pair) {
            var selected = '';
            if (pair.key == this.GMTOffset) {
                selected = ' selected="selected"';
            }
            timeZoneOptions += '<option'+selected+' value="'+pair.key+'">'+pair.value+'</option>';
        }, this);
        var calendarControls = '<div class="week-calendar-controls"><select id="weekCalendarTimeZone">'+timeZoneOptions+'</select><input type="button" id="weekCalendarPreviousButton" value="<"> <input type="button" id="weekCalendarNextButton" value=">"></div>';
        //render calendar header
        var calendarHeaderHtml = "<table class=\"week-calendar-header\"><tbody><tr><td class=\"time-column-header\"></td>";
        for(var i=1 ; i<=7; i++) {
            calendarHeaderHtml += "<td class=\"day-column-header day-" + i + "\"></td>";
        }
        calendarHeaderHtml += "<td class=\"scrollbar-shim\"></td></tr></tbody></table>";

        //render calendar body
        var calendarBodyHtml = "<div id=\"calendar_scroll_grid\" class=\"calendar-scrollable-grid\">\
            <table class=\"week-calendar-time-slots\">\
            <tbody>\
            <tr>\
            <td class=\"grid-timeslot-header\"></td>\
            <td colspan=\"7\">\
            <div class=\"time-slot-wrapper\">\
            <div class=\"time-slots\" id=\"grid-time-slots\">";

        for(var i=this.options.startHour; i!=(this.options.stopHour+1); i++) {
            for(var j=0;j<this.options.timeslotsPerHour - 1; j++) {
                calendarBodyHtml += "<div class=\"time-slot\">t</div>";
            }
            calendarBodyHtml += "<div class=\"time-slot hour-end\" id=\"grid-time-slot-hour-end-"+i+"\"></div>";
        }

        calendarBodyHtml += "</div></div></td></tr><tr><td class=\"grid-timeslot-header\" id=\"grid-timeslot-hour-headers\">";

        for(var i=this.options.startHour; i!=(this.options.stopHour+1); i++) {
            calendarBodyHtml += "<div class=\"hour-header\" id=\"grid-timeslot-hour-header-"+i+"\">\
                    <div class=\"time-header-cell\" id=\"grid-timeslot-hour-cell-"+i+"\">" + this.hourForIndex(i) + "<span class=\"am-pm\">" + this.amOrPm(i) + "</span></div></div>";
        }

        calendarBodyHtml += "</td>";

        for(var i=1 ; i<=7; i++) {
            calendarBodyHtml += "<td class=\"day-column day-" + i + "\" id=\"grid-day-colum-" + i + "\"><div class=\"day-column-inner\" id=\"grid-day-column-inner-"+i+"\"></div></td>"
        }

        calendarBodyHtml += "</tr></tbody></table></div>";
        this.calendarContainer.update(calendarControls + calendarHeaderHtml + calendarBodyHtml, {position: 'top'});

        /**
         * Set the size of the calendar-scrollable-grid
         * each element is 20px plus add 2px to the total for border
         */
        $('calendar_scroll_grid').setStyle({'height' : ((((this.options.stopHour - this.options.startHour) + 1)*20)+2)+'px'});

        weekDayColumns = el.select("div.day-column-inner");
        
        weekDayColumns.each(function(val) {
            val.setStyle({ height: (this.options.timeslotHeight * this.options.timeslotsPerHour) + 'px'});
        }, this);

        el.select("div.time-slot").each(function(val) {
            val.setStyle({ height: (this.options.timeslotHeight -1) + 'px' }); //account for border
        }, this);

        el.select("div.time-header-cell").each(function(val) {
            val.setStyle({
                height :  ((this.options.timeslotHeight * this.options.timeslotsPerHour) - 3) + 'px',
                paddingTop: '1px',
                paddingBottom: '1px',
                paddingRight: '4px'
                });
        }, this);

        // Add the event listener for click events
        Event.observe(el, 'click', this.calenderOnClick.bindAsEventListener(this));

        // Add the div we use for click events where the time is selected
        var clickDivHTML = '<div id="weekCalendarDrillDown" style="display:none;" class="week-calendar-drilldown"><div class="week-calendar-drilldown-close">X</div><div class="week-calendar-drilldown-header">Select an interview time</div></div>'
        this.calendarContainer.insert(clickDivHTML, { position: 'after' });
        var dd = $('weekCalendarDrillDown');
        for(var i=0;i<this.options.drillDownSlots;i++) {
            dd.insert('<div id="ddTimeSlot_'+(i+1)+'" class="week-calendar-drilldown-item">Time:</div>', {position:'bottom'});
            Event.observe($('ddTimeSlot_'+(i+1)), 'mouseover', this.calendarDrillDownOnMouseOver.bindAsEventListener(this));
        }
        
        // Add an event to observe the mouse out of the calendar drill down
        Event.observe($('weekCalendarDrillDown'), 'mouseout', this.calendarDrillDownOnMouseOut.bindAsEventListener(this));
        Event.observe($('weekCalendarDrillDown'), 'mouseover', this.calendarDrillDownOnMouseOver.bindAsEventListener(this));
        Event.observe($('weekCalendarDrillDown'), 'click', this.calendarDrillDownOnClick.bindAsEventListener(this));

        // Add events to observe the navigation buttons
        Event.observe($('weekCalendarPreviousButton'), 'click', this.navigatePrevious.bindAsEventListener(this));
        Event.observe($('weekCalendarNextButton'), 'click', this.navigateNext.bindAsEventListener(this));
        
        // Add events to observer time zone changes
        Event.observe($('weekCalendarTimeZone'), 'change', this.timeZoneChange.bindAsEventListener(this));

        this.loadCalEvents();
        
        if (this.options.onRender) {
            this.options.onRender(this);
        }
    },


    /**
     * Sets the hour slots in the already rendered calendar
     * Can be used to re-size the calendards hours up or down dynamically
     *
     * @param   integer     startHour       Hour of the day to start the calendar at
     * @param   integer     stopHour        Hour of the day to stop the calendar at
     * @param   integer     offset          Timezone offset the start and stop hour where calculated in
     * @returns void
     */
    setHourSlots : function (startHour, stopHour, offset) {
        var diff = offset - (parseInt(this.GMTOffset, 10) * 60);
        if (diff) {
            diff = diff / 60;
        }
        
        // Time to start and stop the grid display
        displayStartHour = parseInt(startHour) - diff;
        displayStopHour = parseInt(stopHour) - diff;
        // Set the weekview start and stop hours
        startHour = parseInt(startHour);
        stopHour = parseInt(stopHour);
        this.options.startHour = displayStartHour;
        this.options.stopHour = stopHour;
        
        for(var i=0;i<25;++i) {
            
            // If the start hour is less then this then delete the existing header
            if ( (i<9 || i>17) &&
                (i < displayStartHour|| i > displayStopHour)) {
                // Remove unused times
                if ($('grid-timeslot-hour-header-'+i)) {
                    $('grid-timeslot-hour-header-'+i).remove();
                }
                if ($('grid-time-slot-hour-end-'+i)) {
                    $('grid-time-slot-hour-end-'+i).remove();
                }                
                // Remove unused slots
                for(var t=0 ; t<7; t++) {
                    if ($('timeslot-'+t+'-'+i)) {
                        $('timeslot-'+t+'-'+i).remove();
                    }
                }
            }
            if (i >= displayStartHour && i <= displayStopHour) {
                // Create the time hour headers
                var pos = null;
                var target = null;
                var slots = new Array();
                // Build and array of the hour headers to find out where we need to insert new ones
                $('grid-timeslot-hour-headers').select('div.hour-header').each(function(item) {
                    var id = item.id;
                    var ar = id.split('grid-timeslot-hour-header-');
                    var hour = parseInt(ar[1]);
                    slots.push(hour);
                })
                for(var s=0;s<slots.length;++s) {
                    if(! pos && i<slots[s]) {
                        pos = 'before'
                        target = $('grid-timeslot-hour-header-'+slots[s]);
                    }
                    if(! pos
                       && i>slots[s]
                       && i<slots[s+1]) {
                        pos = 'after'
                        target = $('grid-timeslot-hour-header-'+slots[s]);
                    }
                }
                if(! pos ) {
                    pos = 'after'
                    if (slots[s]) {
                        target = $('grid-timeslot-hour-header-'+slots[s]);
                    } else if (slots[s-1]) {
                        target = $('grid-timeslot-hour-header-'+slots[s-1]);
                    }
                }
                // Add the new hour header if it does not already exist                
                if ( ! $('grid-timeslot-hour-header-'+i)) {
                    var inserter = new Object;
                    inserter[pos] = "<div class=\"hour-header\" id=\"grid-timeslot-hour-header-"+i+"\">\
                                    <div class=\"time-header-cell\" id=\"grid-timeslot-hour-cell-"+i+"\">" + this.hourForIndex(i) + "<span class=\"am-pm\">" + this.amOrPm(i) + "</span></div></div>";
                    target.insert(inserter);
                    $("grid-timeslot-hour-cell-"+i).setStyle({
                                                                height :  ((this.options.timeslotHeight * this.options.timeslotsPerHour) - 3) + 'px',
                                                                paddingTop: '1px',
                                                                paddingBottom: '1px',
                                                                paddingRight: '4px'
                                                                });
                }
                
                // Create the time slots div
                pos = null;
                target = null;
                slots = new Array();
                // Build and array of the time slot divs to find out where we need to insert new ones
                $('grid-time-slots').select('div.time-slot').each(function(item) {
                    var id = item.id;
                    var ar = id.split('grid-time-slot-hour-end-');
                    var hour = parseInt(ar[1]);
                    slots.push(hour);
                })
                for(var s=0;s<slots.length;++s) {
                    if(! pos && i<slots[s]) {
                        pos = 'before'
                        target = $('grid-time-slot-hour-end-'+slots[s]);
                    }
                    if(! pos
                       && i>slots[s]
                       && i<slots[s+1]) {
                        pos = 'after'
                        target = $('grid-time-slot-hour-end-'+slots[s]);
                    }
                }
                if(! pos ) {
                    pos = 'after'
                    if (slots[s]) {
                        target = $('grid-time-slot-hour-end-'+slots[s]);
                    } else if (slots[s-1]) {
                        target = $('grid-time-slot-hour-end-'+slots[s-1]);
                    }
                }                
                
                if ( ! $('grid-time-slot-hour-end-'+i)) {
                    var inserter = new Object;
                    inserter[pos] = "<div class=\"time-slot hour-end\" id=\"grid-time-slot-hour-end-"+i+"\"></div>";
                    target.insert(inserter);
                    $("grid-time-slot-hour-end-"+i).setStyle({ height: (this.options.timeslotHeight -1) + 'px' }); //account for border
                }
                
                
                // Create the blank time slot
                for(var t=0;t<7; t++) {
                    cl = 'time-slot-unavailable';
                    var d = $('timeslot-'+t+'-'+i);
                    if (!d){
                        pos = null;
                        target = null;
                        slots = new Array();
                        // Build and array of the time slot divs to find out where we need to insert new ones
                        $('grid-day-column-inner-'+(t+1)).select('div').each(function(item) {
                            var id = item.id;
                            var ar = id.split('timeslot-'+t+'-');
                            var hour = parseInt(ar[1]);
                            slots.push(hour);
                        })
                        for(var s=0;s<slots.length;++s) {
                            if(! pos && i<slots[s]) {
                                pos = 'before'
                                target = $('timeslot-'+t+'-'+slots[s]);
                            }
                            if(! pos
                               && i>slots[s]
                               && i<slots[s+1]) {
                                pos = 'after'
                                target = $('timeslot-'+t+'-'+slots[s]);
                            }
                        }
                        if(! pos ) {
                            pos = 'after'
                            if (slots[s]) {
                                target = $('timeslot-'+t+'-'+slots[s]);
                            } else if (slots[s-1]) {
                                target = $('timeslot-'+t+'-'+slots[s-1]);
                            } else {
                                pos = 'bottom';
                                target = $('grid-day-column-inner-'+(t+1));
                            }
                        }                        
                        var inserter = new Object;
                        inserter[pos] = '<div id="timeslot-'+t+'-'+i+'" style="height:'+(this.options.timeslotHeight-2)+'px;" class="'+cl+'"></div>';
                        target.insert(inserter);

                    }
                }                
            }
        }

        //// Set the time header heights
        //$(this.options.container).select("div.time-header-cell").each(function(val) {
        //     val.setStyle({
        //         height :  ((this.options.timeslotHeight * this.options.timeslotsPerHour) - 3) + 'px',
        //         paddingTop: '1px',
        //         paddingBottom: '1px',
        //         paddingRight: '4px'
        //         });
        // }, this);
       
        /**
         * Set the size of the calendar-scrollable-grid
         * each element is 20px plus add 2px to the total for border
         */
        sizeHour = 8
        if ( displayStartHour <= 8 ) {
            sizeHour = displayStartHour - 1
        }
        $('calendar_scroll_grid').setStyle({'height' : ((((this.options.stopHour - sizeHour) + 1)*20)+2)+'px'});
       
    
    },
    

    /**
     * Set the open slots as available
     *
     * @param   object  openTimeSlots       Open time slot object
     * @param   integer timezoneOffset      Timezone offset in minutes that the open time slot dates are in
     * @param   string  timezoneOffsetString    Timezone offset string "-04:00"
     * @return  void
     */
    setOpenSlots : function (openTimeSlots, timezoneOffset, timezoneOffsetString) {
        this.hideNoSlots();
        var slotsOpened = 0;
        this.openSlots = openTimeSlots;
        if(!openTimeSlots) { return; }
        var d = this.cloneDate(this.options.date);
        var ed = this.addDays(this.cloneDate(this.options.date), 6);
        while(!(d > ed)) {
            if (openTimeSlots[this.dateMDY(d, '/')]) {
                // Look for each time slot
                var t = new Date();
                t.setFullYear(d.getFullYear(), d.getMonth(), d.getDate());
                t.setHours(this.options.startHour, 0, 0, 0);
                var et = new Date();
                et.setFullYear(d.getFullYear(), d.getMonth(), d.getDate());
                et.setHours(this.options.stopHour, 0, 0, 0);
                var etl = et;
                etl = etl.add(this.options.scheduleInterval, 'minutes');
                while (t < etl) {
                    var t2;
                    if (timezoneOffsetString == this.GMTOffset) {
                        t2 = t;
                    } else {
                        t2 = this.time2Zone(t, timezoneOffset);
                    }
                    if (openTimeSlots[this.dateMDY(d, '/')]
                        && openTimeSlots[this.dateMDY(d, '/')][this.timeHISZ(t2, timezoneOffsetString)]) {
                        var slotObj = openTimeSlots[this.dateMDY(d, '/')][this.timeHISZ(t2, timezoneOffsetString)];
                        this.setSlotAvailable(t, slotObj.slot_count);
                        ++slotsOpened;
                    }
                    t = t.add(this.options.scheduleInterval, 'minutes');
                }
            }
            d = this.addDays(d, 1);
        }
        // If no slots where opened then show no slots message
        if (! slotsOpened ) {
            this.showNoSlots();
        }
        
    },
    
    /**
     * Adjusts a dates time to the zone specified
     *
     * @param   date    d       Date to edit
     * @param   integer z       Timezone offset in minutes (PHP style aka negative offset to the left of GMT)
     * Note that the javascript getTimezoneOffset() returns the offset in negative values to the right of GMT
     * @returns date        Modified date with the minutes and or hours adjusted. The timezone does not get updated.
     */
    time2Zone : function (d, z) {
        d = this.cloneDate(d);
        z = z * -1;
        var offset = d.getTimezoneOffset();
        var add = offset - z;
        d = d.add( add, 'minutes')
        return d;
    },
    
    /**
     * Set the closed slots
     *
     * @param   object  closedTimeSlots       Closed time slot object
     * @return  void
     */    
    setClosedSlots : function (closedTimeSlots) {
        this.closedSlots = closedTimeSlots;
    },

    /**
     * Called when the navigatePrevious button is pressed
     *
     * @param   object  event       Event that called this function 
     * @return  void
     */    
    navigatePrevious : function(event) {
        var currentDay = this.cloneDate(this.options.date);
        this.options.date = this.removeDays(currentDay, 7);        
        var endDay = this.addDays(this.cloneDate(this.options.date), 6);
        this.loadCalEvents()
        this.hideNoSlots();
        this.showLoading();        
        if (this.options.onNavigatePrevious) {
            this.options.onNavigatePrevious(this, event, this.cloneDate(this.options.date), endDay, this.options.startHour, this.options.stopHour);
        }
        this.hideLoading();        
    },

    /**
     * Called when the navigateNext button is pressed
     *
     * @param   object  event       Event that called this function 
     * @return  void
     */    
    navigateNext : function(event) {
        var currentDay = this.cloneDate(this.options.date);
        this.options.date = this.addDays(currentDay, 7);
        var endDay = this.addDays(this.cloneDate(this.options.date), 6);
        this.loadCalEvents();
        this.hideNoSlots();
        this.showLoading();
        if (this.options.onNavigateNext) {
            this.options.onNavigateNext(this, event, this.cloneDate(this.options.date), endDay, this.options.startHour, this.options.stopHour)
        }
        this.hideLoading();
    },

    /**
     * Shows the calendar loading div
     *
     * @returns void
     */
    showLoading : function() {
        var p = $('calendar_scroll_grid').positionedOffset();
        var h = $('calendar_scroll_grid').getHeight();
        var w = $('calendar_scroll_grid').getWidth();
        if ( ! $('calendar_loading') ) {
            this.options.container.insert({'bottom' : new Element('DIV', {'id': 'calendar_loading', 'style' : 'position:absolute;background-color:#CFCFCF;font-size:48px;font-weight:bold;text-align:center;vertical-align:middle;'}) });
            $('calendar_loading').update('<p style="line-height: '+h+'px;">Loading....</p>');
        }
        $('calendar_loading').setStyle({'left': p[0]+'px', 'top': p[1]+'px', 'width': w+'px', 'height': h+'px'});
        $('calendar_loading').setOpacity('.5');
        $('calendar_loading').show();
        $('calendar_loading').update('<p style="line-height: '+(h-100)+'px;">Loading....</p>');        
    },
    
    /**
     * Hides the calendar loading div
     *
     * @returns void
     */
    hideLoading : function () {
        if ( $('calendar_loading') ) {
            $('calendar_loading').hide();
        }
    },
    
    /**
     * Show the no slots message div
     *
     * @returns void
     */
    showNoSlots : function() {
        var p = $('calendar_scroll_grid').positionedOffset();
        var h = $('calendar_scroll_grid').getHeight();
        var w = $('calendar_scroll_grid').getWidth();
        if ( ! $('calendar_no_slots') ) {
            this.options.container.insert({'bottom' : new Element('DIV', {'id': 'calendar_no_slots', 'style' : 'position:absolute;background-color:#CFCFCF;font-size:48px;font-weight:bold;text-align:center;vertical-align:middle;'}) });
            $('calendar_no_slots').update('<p style="line-height: '+h+'px;">No Open Times</p>');
        }
        $('calendar_no_slots').setStyle({'left': p[0]+'px', 'top': p[1]+'px', 'width': w+'px', 'height': h+'px'});
        $('calendar_no_slots').setOpacity('.5');
        $('calendar_no_slots').show();
        $('calendar_no_slots').update('<p style="line-height: '+(h-100)+'px;">No Open Times</p>');        
    },
    
    /**
     * Hides the no slots message div
     *
     * @returns void
     */    
    hideNoSlots : function() {
        if ( $('calendar_no_slots') ) {
            $('calendar_no_slots').hide();
        }
    },
    
    /**
     * Called when the time zone drop down changes
     *
     * Event listener
     *
     * @param   events      event       Event that triggered the change
     * @returns void
     */
    timeZoneChange : function(event) {
        event = event || window.event
        var target = event.target || event.srcElement
        this.GMTOffset = target.value;
        
        var currentDay = this.cloneDate(this.options.date);
        var endDay = this.addDays(this.cloneDate(this.options.date), 6);
        
        this.setAvailabiltySlots(currentDay);
 
        if (this.options.onTimeZoneChange) {
            this.options.onTimeZoneChange(this, event, this.cloneDate(this.options.date), endDay, this.options.startHour, this.options.stopHour)
        }
    },
    
    /**
     * Called when a drill down time slot is clicked on
     *
     * @param   event      event    Event that triggred this function on click
     * @returns void
     */
    calenderOnClick : function(event) {
        event = event || window.event
        var target = event.target || event.srcElement
        if(!target.id.match(/^timeslot/) ||
           !target.hasClassName('time-slot-available')) { return; }
        var dd = $('weekCalendarDrillDown');
        var arTargetParts = target.id.split('-');
        var slotStart, slotStop;
        for(var i=parseInt(arTargetParts[2], 10);
            i<(this.options.stopHour + 1);
            i++) {
            var ts = $(arTargetParts[0]+'-'+arTargetParts[1]+'-'+i);
            if(ts && ts.hasClassName('time-slot-available')) {
                slotStop=i;
            } else { break; }
        }
        for(var i=parseInt(arTargetParts[2], 10);
            i>(this.options.startHour - 1);
            i--) {
            var ts = $(arTargetParts[0]+'-'+arTargetParts[1]+'-'+i);
            if(ts && ts.hasClassName('time-slot-available')) {
                slotStart=i;
            } else { break; }
        }
        var minTime = this.options.reserveTime;
        var maxTime = minTime + this.options.breakTime;
        
        var targetTime = arTargetParts[2];
        var targetTimeL = targetTime;
        var ampm = 'AM';
        if (targetTime >= 12) { ampm = 'PM'; }
        if (targetTime > 12) { targetTimeL = targetTime - 12;}
        if (targetTimeL == 0) { targetTimeL = 12; }
        var x=0;
        for(var i=0;i<this.options.drillDownSlots;i++) {
            var ddTs = $('ddTimeSlot_'+(i+1));
            var xs = x;
            if (xs < 10) { xs = '0'+x; }
            ddTs.update(targetTimeL+':'+xs+' '+ampm);
            // Calculate and store the target date and time in the element as .timeSlotDate
            var tsd = this.cloneDate(this.options.date);
            tsd = this.addDays(tsd, parseInt(arTargetParts[1], 10));
            tsd.setHours(parseInt(targetTime, 10), parseInt(x, 10));
            ddTs.timeSlotDate = tsd;
            Element.show(ddTs);
            // Check run under
            if (parseInt(arTargetParts[2], 10) == slotStart && x < maxTime && x > (minTime / 2)) {
                Element.hide(ddTs);
                //ddTs.update('<div style="background-color:red; color:white;">rununder</div>');
            }
            // Check the run over
            if (parseInt(arTargetParts[2], 10) == slotStop && ((60-x) < minTime)) {
                //ddTs.update('<div style="background-color:red; color:white;">runover</div>');
                Element.hide(ddTs);
            }
            // Keep last
            x = Math.round((60 / this.options.drillDownSlots) * (i+1));
        }
        // Determin the left offset of the dd element and if it goes beyond the viewport size then move it to the left
        var ddleft = event.clientX-5;
        if ( (dd.getWidth()+event.clientX) > document.viewport.getWidth() ) {
            ddleft = (ddleft - dd.getWidth());
        }
        dd.show();
        dd.setStyle({top:(event.clientY-5)+'px', left:(ddleft)+'px'})
        
    },
    
    /**
     * Called when the mouse out happens for a calendar drill down element
     *
     * @param   object  event       Event that called this function 
     * @return  void
     */       
    calendarDrillDownOnMouseOut : function(event) {
        event.preventDefault();
        event = event || window.event
        var target = event.target || event.srcElement
        this.drillDownTimeOut = setTimeout("$('weekCalendarDrillDown').hide()", 1000)
    },
    
    /**
     * Called when the mouse over happens for a calendar drill down element
     *
     * @param   object  event       Event that called this function 
     * @return  void
     */       
    calendarDrillDownOnMouseOver  : function(event) {
        event = event || window.event
        var target = event.target || event.srcElement
        if (this.drillDownTimeOut) {
            clearTimeout(this.drillDownTimeOut)
        }
    },
    
    /**
     * Called when the click event happens for a calendar drill down element
     *
     * @param   object  event       Event that called this function 
     * @return  void
     */       
    calendarDrillDownOnClick : function(event) {
        event = event || window.event
        var target = event.target || event.srcElement
        $('weekCalendarDrillDown').hide()
        if (this.options.onSlotSelect
            && target.timeSlotDate) {
            this.options.onSlotSelect(this, event, target.timeSlotDate);
        }
    },
    
    /**
     * Set a time slot as unavailable
     *
     * @param   integer  iDay   Day number to set
     * @param   integer  iHour  Hour number to set
     * @return  void
     */       
    setTimeSlotUnavailable : function(iDay, iHour) {
        var ts = $('timeslot-'+iDay+'-'+iHour);
        if (ts) {
            ts.removeClassName('time-slot-available');
            ts.removeClassName('time-slot-full');
            ts.addClassName('time-slot-unavailable');
        }
    },
    
    /**
     * Set a time slot as full
     *
     * @param   integer  iDay   Day number to set
     * @param   integer  iHour  Hour number to set
     * @return  void
     */       
    setTimeSlotFull : function(iDay, iHour) {
        var ts = $('timeslot-'+iDay+'-'+iHour);
        if (ts) {
            ts.removeClassName('time-slot-available');
            ts.removeClassName('time-slot-unavailable');
            ts.addClassName('time-slot-full');
        }
    },    

    /**
     * Set a time slot as available
     *
     * @param   integer  iDay   Day number to set
     * @param   integer  iHour  Hour number to set
     * @return  void
     */       
    setTimeSlotAvailable : function(iDay, iHour) {
        var ts = $('timeslot-'+iDay+'-'+iHour);
        if (ts) {
            ts.removeClassName('time-slot-full');
            ts.removeClassName('time-slot-unavailable');
            ts.addClassName('time-slot-available');
        }
    },
    
    /**
     * Resize the calendar (not supported yet)
     *
     * @return  void
     */           
    resizeCalendar : function() {

    },

    /**
     * Load all the calendar events for a date
     *
     * @param   integer     dateWithinWeek      Date of the week to load events for
     *
     * @return  void
     */           
    loadCalEvents : function(dateWithinWeek) {
        var options, date, weekStartDate, weekEndDate, weekDayColumns;
        options = this.options;
        date = dateWithinWeek || options.date;
        weekStartDate = date;
        weekEndDate = this.dateOneWeek(date);
        options.startDate = weekStartDate;
        options.endDate = weekEndDate;
        weekDayColumns = options.container.select("div.day-column-inner");
        this.updateDayColumnHeader(weekDayColumns);
        this.setAvailabiltySlots(date);
    },

    /**
     * Update the column headers for all the currenlty displayed days
     *
     * @param   array of elements     weekDayColumns      Array of HTML column elements
     * @return  void
     */ 
    updateDayColumnHeader : function(weekDayColumns) {
        var currentDay = this.cloneDate(this.options.date);
        this.options.container.select("table.week-calendar-header td.day-column-header").each(function(val, i) {
                val.update(this.dayNames[currentDay.getDay()] + "<br/>" + this.monthNames[currentDay.getMonth()] + ", " + currentDay.getDate() + ", " + currentDay.getFullYear());
                if(this.isToday(currentDay)) {
                    val.addClassName("today");
                } else {
                    val.removeClassName("today");
                }
                currentDay = this.addDays(currentDay, 1);

        }, this)
        var currentDay = this.cloneDate(this.options.date);
        weekDayColumns.each(function(val,i) {
            this.options.startDate =  this.cloneDate(currentDay);
            this.options.endDate = new Date(currentDay.getTime() + (this.MILLIS_IN_DAY - 1));
            if(this.isToday(currentDay)) {
                val.up().addClassName("today");
            } else {
                val.up().removeClassName("today");
            }
            currentDay = this.addDays(currentDay, 1);
        }, this);
    },

    /**
     * Set a time slot as available
     *
     * @param   Date    sDate       Date and time to set as available
     * @param   integer iSlotCount  Number of slots open in this time slot
     * @return  void
     */
    setSlotAvailable : function (sDate, iSlotCount) {
        // Calculate the offset of the time being set as available to the calendars timezone offset
        var tzOffset = 0;
        if ( parseInt(this.GMTOffset, 10) < parseInt(sDate.getGmtOffset(), 10)) {
            tzOffset =   parseInt(sDate.getGmtOffset(), 10) - parseInt(this.GMTOffset, 10);
            tzOffset = tzOffset * -1;
        }
        var currentDay = this.cloneDate(this.options.date);
        var compareDay = this.cloneDate(sDate);
        compareDay = this.clearTime(compareDay);
        var x=0;
        while (currentDay.getTime() < compareDay.getTime()) {
            currentDay = this.addDays(currentDay, 1)
            x++;
        }
        var d = $('timeslot-'+x+'-'+(sDate.getHours() + tzOffset));
        if (d) {
            d.removeClassName('time-slot-unavailable');
            d.addClassName('time-slot-available');
            d.update('Available')
            
        }
    },
    
    /**
     * Set a time slot as not available
     *
     * @param   Date    sDate       Date and time to set as unavailable
     * @param   integer iSlotCount  Number of slots taken in this time slot
     * @return  void
     */
    setSlotUnavailable : function (sDate, iSlotCount) {
        // Calculate the offset of the time being set as unavailable to the calendars timezone offset
        var tzOffset =   parseInt(sDate.getGmtOffset(), 10) -  parseInt(this.GMTOffset, 10);
        tzOffset = tzOffset * -1;
        var currentDay = this.cloneDate(this.options.date);
        var compareDay = this.cloneDate(sDate);
        compareDay = this.clearTime(compareDay);
        var x=0;
        while (currentDay.getTime() < compareDay.getTime()) {
            currentDay = this.addDays(currentDay, 1)
            x++;
        }
    },
    
    /**
     * Set all the slots for a day to unavailable
     *
     * @param   Date    sDate       Date to set
     * @return  void
     */
    setAvailabiltySlots : function(date) {
        this.clearTime(date);
        var weekDayColumns = this.options.container.select("div.day-column-inner");
        var currentDay = this.cloneDate(this.options.date);
        weekDayColumns.each(function(val,i) { // For each column
            for(var k=0; k!=(this.options.stopHour+1); k++) { // For each hour
                for(var j=0;j<this.options.timeslotsPerHour; j++) { // For each time slot per hour
                    cl = 'time-slot-unavailable';
                    var d = $('timeslot-'+i+'-'+k);
                    if (d) {
                        // If the time slot exists then set it to blank
                        d.removeClassName('time-slot-available');
                        d.removeClassName('time-slot-full');
                        d.addClassName('time-slot-unavailable');
                        d.update('');
                    }                    
                }
            }
            currentDay = this.addDays(currentDay, 1);
        }, this);
    },

    /**
     * Format a date as time
     *
     * @param   Date    date       Date to format
     * @return   string  Time string "HH:MM AM|PM"
     */    
    formatAsTime : function(date) {
        return zeroPad(hourForIndex(date.getHours()), 2) + ":" + zeroPad(date.getMinutes(), 2) + " " + amOrPm(date.getHours());
    },

    /**
     * Return the hour for an index where
     * 0 = 12,
     * 1 - 12 = 1 - 12
     * 13 - 23 = x - 12
     *
     * @param   integer index   Index to get the hour for
     * @return   integer Hour value
     */ 
    hourForIndex : function(index) {
        if(index === 0 ) { //midnight
            return 12;
        } else if(index < 13) { //am
            return index;
        } else { //pm
            return index - 12;
        }
    },

    /**
     * Get the AM or PM value based on an hour
     *
     * @param   integer hourOfDay   Hour of day to get AM / PM for
     * @return   string AM | PM
     */ 
    amOrPm : function(hourOfDay) {
        return hourOfDay < 12 ? "AM" : "PM";
    },

    /**
     * Check if a date is today
     *
     * @param   date date   Date to check
     * @return   boolean     True if the date is today
     */ 
    isToday : function(date) {
        var clonedDate = this.cloneDate(date);
        this.clearTime(clonedDate);
        var today = new Date();
        this.clearTime(today);
        return today.getTime() === clonedDate.getTime();
    },

    /**
     * Take a date and add one week to it
     *
     * @param   date date   Date to add to
     * @return   date     Date plus one week
     */ 
    dateOneWeek : function(date) {
        var midnightCurrentDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
        var millisToAdd = (6 * this.MILLIS_IN_DAY);
        return new Date(midnightCurrentDate.getTime() + millisToAdd);
    },

    /**
     * Take a date set it to the first day of the dates week
     *
     * @param   date date   Date to set
     * @return   date     Date set to first day of that dates week
     */
    dateFirstDayOfWeek : function(date) {
        var midnightCurrentDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
        var currentDayOfWeek = midnightCurrentDate.getDay();
        var millisToSubtract = currentDayOfWeek * 86400000;
        return new Date(midnightCurrentDate.getTime() - millisToSubtract);
    },

    /**
     * Take a date set it to the last day of the dates week
     *
     * @param   date date   Date to set
     * @return   date     Date set to last day of that dates week
     */
    dateLastDayOfWeek : function(date) {
        var midnightCurrentDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
        var currentDayOfWeek = midnightCurrentDate.getDay();
        var millisToAdd = (6 - currentDayOfWeek) * this.MILLIS_IN_DAY;
        return new Date(midnightCurrentDate.getTime() + millisToAdd);
    },

    /**
     * Take a date set it to the last millisecond of the dates week
     *
     * @param   date date   Date to set
     * @return   date     Date set to last millisecond of that dates week
     */
    dateLastMilliOfWeek : function(date) {
        var lastDayOfWeek = this.dateLastDayOfWeek(date);
        return new Date(lastDayOfWeek.getTime() + (this.MILLIS_IN_DAY - 1));
    },

    /**
     * Zero pad a number
     *
     * @param   integer     number  Number to zero pad
     * @param   interger    size    Size to zero pad to
     * @return  string      Zero padded number as a string
     */
    zeroPad : function(number, size) {
        var length = ("" + number).length;
        var strNumber = "" + number;
        for(var i = 0; i<size - length; i++) {
            strNumber = "0" + strNumber;
        }
        return strNumber;
    },

    /**
     * Clone a date and return a new date object to dereference it
     *
     * @param   date     d      Date to clone
     * @return  date            Dereferenced cloned date
     */
    cloneDate : function(d) {
        return new Date(+d);
    },

    /**
     * Clean a date, if its a string then parse it, if its a number then treat it as a time stamp
     *
     * @param   mixed     d     Date to clean either a string version of a date or an integer version of a date
     * @return  date            Cleaned  date
     */
    cleanDate : function(d) {
        if (typeof d == 'string')
            return this.parseISO8601(d, true) || Date.parse(d) || new Date(parseInt(d, 10));
        if (typeof d == 'number')
            return new Date(d);
        return d;
    },

    /**
     * Remove the time from a date object
     *
     * @param   date     d     Date to remove time from 
     * @return  date            Cleaned date with zero time
     */
    clearTime : function(d) {
        d.setHours(0);
        d.setMinutes(0);
        d.setSeconds(0);
        d.setMilliseconds(0);
        return d;
    },

    /**
     * Add a number of days to a date
     *
     * @param   date     d     Date to add days to
     * @param   integer  n     Number of days to add
     * @param   boolean  keepTime   If false then the time will be cleared, otherwise it will be kept
     * @return  date            Cleaned date days added and time optionally cleared
     */
    addDays : function(d, n, keepTime) {
        d.setDate(d.getDate() + n);
        if (keepTime) return d;
        return this.clearTime(d);
    },

    /**
     * Remove number of days form a date
     *
     * @param   date     d     Date to remove days from
     * @param   integer  n     Number of days to remove
     * @param   boolean  keepTime   If false then the time will be cleared, otherwise it will be kept
     * @return  date            Cleaned date days removed and time optionally cleared
     */
    removeDays : function(d, n, keepTime) {
        d.setDate(d.getDate() - n);
        if (keepTime) return d;
        return this.clearTime(d);
    },

    /**
     * Parse an ISO8610 date string
     *
     * @param   string     s     Date string to parse
     * @param   boolean  ignoreTimezone   If true then the time zone will be igonred and the default will be used
     * @return  date            Date verison of the string
     */
    parseISO8601 : function(s, ignoreTimezone) {
        // derived from http://delete.me.uk/2005/03/iso8601.html
        var regexp = "([0-9]{4})(-([0-9]{2})(-([0-9]{2})" +
            "(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?" +
            "(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?";
        var d = s.match(new RegExp(regexp));
        if (!d) return null;
        var offset = 0;
        var date = new Date(d[1], 0, 1);
        if (d[3]) { date.setMonth(d[3] - 1); }
        if (d[5]) { date.setDate(d[5]); }
        if (d[7]) { date.setHours(d[7]); }
        if (d[8]) { date.setMinutes(d[8]); }
        if (d[10]) { date.setSeconds(d[10]); }
        if (d[12]) { date.setMilliseconds(Number("0." + d[12]) * 1000); }
        if (!ignoreTimezone) {
            if (d[14]) {
                offset = (Number(d[16]) * 60) + Number(d[17]);
                offset *= ((d[15] == '-') ? 1 : -1);
            }
            offset -= date.getTimezoneOffset();
        }
        return new Date(Number(date) + (offset * 60 * 1000));
    },

    /**
     * Turn a date into an ISO8610 string
     *
     * @param   date     date     Date to parse
     * @return  string            ISO8610 string verison of the date
     */
    ISO8601String : function(date) {
        // derived from http://delete.me.uk/2005/03/iso8601.html
        var zeropad = function (num) { return ((num < 10) ? '0' : '') + num; }
        return date.getUTCFullYear() +
            "-" + zeropad(date.getUTCMonth() + 1) +
            "-" + zeropad(date.getUTCDate()) +
            "T" + zeropad(date.getUTCHours()) +
            ":" + zeropad(date.getUTCMinutes()) +
            ":" + zeropad(date.getUTCSeconds()) +
            "Z";
    },

    /**
     * Zero pad any number under 10 to the tens place
     *
     * @param   integer n   Number to pad
     */
    zeroPad : function (n) {
        if (n<10) {
            return '0'+n;
        } else {
            return ''+n;
        }
    },

    /**
     * Format a date string as mm-dd-yyyy
     *
     * @param   date    d   Date object
     * @returns string      d as mm-dd-yyyy string
     */
    dateMDY : function(d, delimiter) {
        if (! delimiter) {
            delimiter = '-';
        }
        return this.zeroPad((d.getMonth()+1))+delimiter+this.zeroPad(d.getDate())+delimiter+d.getFullYear();
    },
    
    /**
     * Format a time string as H:i:s (hours:minutes:seconds)
     * H = 24 hour with leading zero
     * i = minutes with leading zero
     * s = second with leading zero
     *
     * @param   date    d   Date object
     * @returns string      d as H:i:s string
     */    
    timeHIS : function(d) {
        return this.zeroPad(d.getHours())+':'+this.zeroPad(d.getMinutes())+':'+this.zeroPad(d.getSeconds());
    },
    
    /**
     * Format a time string as H:i:s Z (hours:minutes:seconds GMT offset)
     * H = 24 hour with leading zero
     * i = minutes with leading zero
     * s = second with leading zero
     * Z = GMT offset ie (-0100)
     *
     * @param   date    d   Date object
     * @param   string  zone   Optional    Time zone offset. Setting is will replace zone with the time zone value you spefify
     * @returns string      d as H:i:s Z string
     */    
    timeHISZ : function(d, zone) {
        if (! zone) {
            zone = d.getGmtOffset()
        }
        return this.zeroPad(d.getHours())+':'+this.zeroPad(d.getMinutes())+':'+this.zeroPad(d.getSeconds()) + ' ' + zone;
    }

});
